/* /* smallchat-client.c -- Client program for smallchat-server.  
 * smallchat-client.c -- 用于 smallchat-server 的客户端程序。
 *
 * Copyright (c) 2023, Salvatore Sanfilippo <antirez at gmail dot com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the project name of nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without
 *     specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/select.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>

#include "chatlib.h"

/* ============================================================================
 * Low level terminal handling. 底层终端处理。
 * ========================================================================== */

void disableRawModeAtExit(void);

/* Raw mode: 1960 magic shit. */ 
/* 原始模式：1960 年的魔法。*/
int setRawMode(int fd, int enable) {
    /* We have a bit of global state (but local in scope) here.
     * This is needed to correctly set/undo raw mode. */ 
     /* 这里有一些全局状态（但作用域是局部的）。
     * 这是为了正确设置/撤销原始模式。*/
    static struct termios orig_termios; // Save original terminal status here. /* 保存原始终端状态。*/
    static int atexit_registered = 0;   // Avoid registering atexit() many times. /* 避免多次注册 atexit()。*/
    static int rawmode_is_set = 0;      // True if raw mode was enabled. /* 原始模式是否已启用。*/

    struct termios raw;

    /* If enable is zero, we just have to disable raw mode if it is
     * currently set. */ /* 如果 enable 为零，我们只需要禁用原始模式（如果它当前已启用）。*/
    if (enable == 0) {
        /* Don't even check the return value as it's too late. */ /* 不要检查返回值，太晚了。*/
        if (rawmode_is_set && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)
            rawmode_is_set = 0;
        return 0;
    }

    /* Enable raw mode. */  /* 启用原始模式。*/
    if (!isatty(fd)) goto fatal;
    if (!atexit_registered) {
        atexit(disableRawModeAtExit);
        atexit_registered = 1;
    }
    if (tcgetattr(fd,&orig_termios) == -1) goto fatal;

    raw = orig_termios;  /* modify the original mode */ /* 修改原始模式 */
    /* input modes: no break, no CR to NL, no parity check, no strip char,
     * no start/stop output control. */ /* 输入模式：无中断、无 CR 到 NL、无奇偶校验、无剥离字符、无开始/停止输出控制。*/
    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
    /* output modes - do nothing. We want post processing enabled so that
     * \n will be automatically translated to \r\n. */ 
    /* 输出模式 - 不做任何操作。
     * 我们希望启用后处理，以便 \n 将自动转换为 \r\n。*/
    // raw.c_oflag &= ... /* 不做任何操作。*/
    /* control modes - set 8 bit chars */ /* 设置 8 位字符 */
    raw.c_cflag |= (CS8);
    /* local modes - choing off, canonical off, no extended functions,
     * but take signal chars (^Z,^C) enabled. */ /* 本地模式 - 关闭回显、规范模式、无扩展功能，但启用信号字符（^Z、^C）。*/
    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN);
    /* control chars - set return condition: min number of bytes and timer.
     * We want read to return every single byte, without timeout. */ 
    /* 控制字符 - 设置返回条件：最小字节数和计时器。
     * 我们希望 read 返回每个字节，而不超时。*/
    raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ /* 1 字节，无计时器 */

    /* put terminal in raw mode after flushing */ /* 刷新后将终端置于原始模式 */
    if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;
    rawmode_is_set = 1;
    return 0;

fatal:
    errno = ENOTTY;
    return -1;
}

/* At exit we'll try to fix the terminal to the initial conditions. */ /* 退出时，我们将尝试将终端恢复到初始条件。*/
void disableRawModeAtExit(void) {
    setRawMode(STDIN_FILENO,0);
}

/* ============================================================================
 * Mininal line editing. 最小行编辑。
 * ========================================================================== */

void terminalCleanCurrentLine(void) {
    write(fileno(stdout),"\e[2K",4);
}

void terminalCursorAtLineStart(void) {
    write(fileno(stdout),"\r",1);
}

#define IB_MAX 128
struct InputBuffer {
    char buf[IB_MAX];       // Buffer holding the data. /* 缓冲区，用于保存数据。*/
    int len;                // Current length. /* 当前长度。*/
};

/* inputBuffer*() return values: */ /* inputBuffer*() 返回值：*/
#define IB_ERR 0        // Sorry, unable to comply. /* 对不起，无法满足。*/
#define IB_OK 1         // Ok, got the new char, did the operation, ... /* 好的，得到了新字符，执行了操作，...*/
#define IB_GOTLINE 2    // Hey, now there is a well formed line to read. /* 嘿，现在有一个完整的行可以读取。*/

/* Append the specified character to the buffer. */ /* 将指定字符附加到缓冲区。*/
int inputBufferAppend(struct InputBuffer *ib, int c) {
    if (ib->len >= IB_MAX) return IB_ERR; // No room. /* 没有房间。*/

    ib->buf[ib->len] = c;
    ib->len++;
    return IB_OK;
}

void inputBufferHide(struct InputBuffer *ib);
void inputBufferShow(struct InputBuffer *ib);

/* Process every new keystroke arriving from the keyboard. As a side effect
 * the input buffer state is modified in order to reflect the current line
 * the user is typing, so that reading the input buffer 'buf' for 'len'
 * bytes will contain it. */
/* 处理来自键盘的每个新按键。
 * 作为副作用，
 * 输入缓冲区状态被修改，以反映用户正在键入的当前行，
 * 因此读取输入缓冲区 'buf' 中的 'len' 字节将包含它。*/
int inputBufferFeedChar(struct InputBuffer *ib, int c) {
    switch(c) {
    case '\n':
        break;          // Ignored. We handle \r instead. /* 忽略。我们处理 \r 而不是 \n。*/
    case '\r':
        return IB_GOTLINE;
    case 127:           // Backspace. /* 退格键。*/
        if (ib->len > 0) {
            ib->len--;
            inputBufferHide(ib);
            inputBufferShow(ib);
        }
        break;
    default:
        if (inputBufferAppend(ib,c) == IB_OK)
            write(fileno(stdout),ib->buf+ib->len-1,1);
        break;
    }
    return IB_OK;
}

/* Hide the line the user is typing. */ /* 隐藏用户正在键入的行。*/
void inputBufferHide(struct InputBuffer *ib) {
    (void)ib; // Not used var, but is conceptually part of the API. /* 未使用的变量，但概念上属于 API。*/
    terminalCleanCurrentLine();
    terminalCursorAtLineStart();
}

/* Show again the current line. Usually called after InputBufferHide(). */ /* 再次显示当前行。通常在 InputBufferHide() 之后调用。*/
void inputBufferShow(struct InputBuffer *ib) {
    write(fileno(stdout),ib->buf,ib->len);
}

/* Reset the buffer to be empty. */
void inputBufferClear(struct InputBuffer *ib) {
    ib->len = 0;
    inputBufferHide(ib);
}

/* =============================================================================
 * Main program logic, finally :) 主程序逻辑，最终 :)
 * ========================================================================== */

int main(int argc, char **argv) {
    if (argc != 3) {
        printf("Usage: %s <host> <port>\n", argv[0]);
        exit(1);
    }

    /* Create a TCP connection with the server. */ /* 创建与服务器的 TCP 连接。*/
    int s = TCPConnect(argv[1],atoi(argv[2]),0);
    if (s == -1) {
        perror("Connecting to server");
        exit(1);
    }

    /* Put the terminal in raw mode: this way we will receive every
     * single key stroke as soon as the user types it. No buffering
     * nor translation of escape sequences of any kind. */ 
    /* 将终端置于原始模式：
     * 这样，当用户键入键时，我们将立即收到每个键。
     * 没有缓冲或任何类型的转义序列的翻译。*/
    setRawMode(fileno(stdin),1);

    /* Wait for the standard input or the server socket to
     * have some data. */ /* 等待标准输入或服务器套接字有一些数据。*/
    fd_set readfds;
    int stdin_fd = fileno(stdin);

    struct InputBuffer ib;
    inputBufferClear(&ib);

    while(1) {
        FD_ZERO(&readfds);
        FD_SET(s, &readfds);
        FD_SET(stdin_fd, &readfds);
        int maxfd = s > stdin_fd ? s : stdin_fd;

        int num_events = select(maxfd+1, &readfds, NULL, NULL, NULL);
        if (num_events == -1) {
            perror("select() error");
            exit(1);
        } else if (num_events) {
            char buf[128]; /* Generic buffer for both code paths. */ /* 通用缓冲区，用于两种代码路径。*/

            if (FD_ISSET(s, &readfds)) {
                /* Data from the server? */ /* 来自服务器的数据？*/
                ssize_t count = read(s,buf,sizeof(buf));
                if (count <= 0) {
                    printf("Connection lost\n");
                    exit(1);
                }
                inputBufferHide(&ib);
                write(fileno(stdout),buf,count);
                inputBufferShow(&ib);
            } else if (FD_ISSET(stdin_fd, &readfds)) {
                /* Data from the user typing on the terminal? */ /* 来自用户在终端上键入的数据？*/
                ssize_t count = read(stdin_fd,buf,sizeof(buf));
                for (int j = 0; j < count; j++) {
                    int res = inputBufferFeedChar(&ib,buf[j]);
                    switch(res) {
                    case IB_GOTLINE:
                        inputBufferAppend(&ib,'\n');
                        inputBufferHide(&ib);
                        write(fileno(stdout),"you> ", 5);
                        write(fileno(stdout),ib.buf,ib.len);
                        write(s,ib.buf,ib.len);
                        inputBufferClear(&ib);
                        break;
                    case IB_OK:
                        break;
                    }
                }
            }
        }
    }

    close(s);
    return 0;
}
